﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Reflection.Emit;
using NMapper.Utils;

namespace NMapper.Mappings
{
    public interface IDiscriminatorMapping<TDatabase> : IDbSelector<TDatabase>
        where TDatabase : IDisposable
    {
        object GetValue(object transient);
        IMapper<TDatabase> this[object value] { get; }
    }

    public class DiscriminatorMapping<TDatabase, TDatabaseEntity, TOutput, TDiscriminator> : IDbValueMapping<TDatabase, TDatabaseEntity, TDiscriminator> , IDiscriminatorMapping<TDatabase>
        where TDatabase : IDisposable
    {
        private Expression<Func<MapContext<TDatabase>, TDatabaseEntity, TDiscriminator>> dbDiscriminatorSelector;
        private Func<TOutput, TDiscriminator> outputDiscriminatorSelector;
        private Func<MapContext<TDatabase>, TDatabaseEntity, TDiscriminator> dbDiscriminatorSelectorFunc;
        
        private PropertyInfo discriminator;
        private Dictionary<TDiscriminator, Tuple<LambdaExpression, IMapper<TDatabase>, Func<MapContext<TDatabase>, TDatabaseEntity, object>>> typeMapping = new Dictionary<TDiscriminator, Tuple<LambdaExpression, IMapper<TDatabase>, Func<MapContext<TDatabase>, TDatabaseEntity, object>>>();
        private Dictionary<IMapper<TDatabase>, PropertyInfo> properties = new Dictionary<IMapper<TDatabase>, PropertyInfo>();

        public DiscriminatorMapping(Expression<Func<MapContext<TDatabase>, TDatabaseEntity, TDiscriminator>> dbDiscriminatorSelector, Func<TOutput, TDiscriminator> outputDiscriminatorSelector)
        {
            this.dbDiscriminatorSelector = dbDiscriminatorSelector;
            this.outputDiscriminatorSelector = outputDiscriminatorSelector;
            dbDiscriminatorSelectorFunc = dbDiscriminatorSelector.Compile();
        }

        LambdaExpression IDbSelector<TDatabase>.DbSelectorExpression
        {
            get { return dbDiscriminatorSelector; }
        }

        Func<MapContext<TDatabase>, object, object> IDbSelector<TDatabase>.DbSelector
        {
            get { return (mapContext, x) => dbDiscriminatorSelectorFunc(mapContext, (TDatabaseEntity)x); }
        }

        Func<MapContext<TDatabase>, TDatabaseEntity, TDiscriminator> IDbSelector<TDatabase, TDatabaseEntity, TDiscriminator>.DbSelector
        {
            get { return dbDiscriminatorSelectorFunc; }
        }

        public PropertyInfo TransientProperty
        {
            get { return discriminator; }
        }

        public void Map<TSubDatabaseEntity, TSubOutput>(TDiscriminator value, Expression<Func<MapContext<TDatabase>, TDatabaseEntity, TSubDatabaseEntity>> dbSelector, Mapper<TDatabase, TSubDatabaseEntity, TSubOutput> subTypeMapper)
            where TSubOutput : class
        {
            var func = dbSelector.Compile();
            typeMapping.Add(value, Tuple.Create<LambdaExpression, IMapper<TDatabase>, Func<MapContext<TDatabase>, TDatabaseEntity, object>>(dbSelector, subTypeMapper, (mapContext, x) => func(mapContext, x)));
        }

        public void Map<TSubDatabaseEntity, TSubOutput>(TDiscriminator value, Expression<Func<TDatabaseEntity, TSubDatabaseEntity>> dbSelector, Mapper<TDatabase, TSubDatabaseEntity, TSubOutput> subTypeMapper)
            where TSubOutput : class
        {
            Map(value, (Expression<Func<MapContext<TDatabase>, TDatabaseEntity, TSubDatabaseEntity>>)dbSelector.PrependParameters(typeof(MapContext<TDatabase>)), subTypeMapper);
        }

        object IDiscriminatorMapping<TDatabase>.GetValue(object transient)
        {
            return GetValue(transient);
        }

        public TDiscriminator GetValue(object transient)
        {
            return (TDiscriminator)discriminator.GetValue(transient, null);
        }

        public IMapper<TDatabase> this[TDiscriminator value]
        {
            get { return typeMapping[value].Item2; }
        }

        IMapper<TDatabase> IDiscriminatorMapping<TDatabase>.this[object value]
        {
            get { return this[(TDiscriminator)value]; }
        }

        public void DefineTransientType(ModuleBuilder module, TypeBuilder type)
        {
            foreach (var item in typeMapping.Values)
            {
                var dbSelector = item.Item1;
                var mapper = item.Item2;
                mapper.Initialize();

                var propertyName = NameHelpers.SuggestName(dbSelector) + "_" + mapper.TransientType.Name;
                var property = type.DefineProperty(propertyName, mapper.TransientType);
                properties[mapper] = property;
            }

            var discriminatorPropertyName = NameHelpers.SuggestName(dbDiscriminatorSelector);
            var dbPropertyPath = dbDiscriminatorSelector.GetPropertyPath();
            var propertyType = dbPropertyPath.Last().PropertyType;
            discriminator = type.DefineProperty(discriminatorPropertyName, propertyType);
        }

        public void DefineTransientInitialization(MapContext<TDatabase> mapContext, Type transientType, List<MemberBinding> initializers, ParameterExpression parameterExpression)
        {
            discriminator = transientType.GetProperty(discriminator.Name);
            var lambdaBinder = new LambdaBinder();
            foreach (var item in typeMapping.Values)
            {
                var dbSelector = item.Item1;
                var mapper = item.Item2;
                var property = properties[mapper] = transientType.GetProperty(properties[mapper].Name);

                var subSelector = mapper.CreateTransientSelector(mapContext);
                var simpleDbSelector = dbSelector.PartialApplication(Expression.Constant(mapContext), null).Simplify();
                var newTarget = lambdaBinder.BindBody(simpleDbSelector, parameterExpression);
                var boundSubSelector = lambdaBinder.BindBody(subSelector, newTarget);

                var nullChecker = new LambdaNullChecker();
                Expression boundSubSelectorWithNullCheck = nullChecker.AddNullCheck(newTarget, boundSubSelector);
            
                var initializer = Expression.Bind(property.GetSetMethod(), boundSubSelectorWithNullCheck);
                initializers.Add(initializer);
            }

            var simpleDbDiscriminatorSelector = dbDiscriminatorSelector.PartialApplication(Expression.Constant(mapContext), null).Simplify();
            var selector = lambdaBinder.Bind(simpleDbDiscriminatorSelector, parameterExpression);
            var lhs = discriminator;
            var rhs = selector.Body;
            initializers.Add(Expression.Bind(lhs.GetSetMethod(), rhs));

        }

        void IMapping<TDatabase>.OnTransientInitialized(MapContext<TDatabase> mapContext, Array transient)
        {
            foreach (var item in transient)
            {
                var value = GetValue(item);
                var mapper = this[value];
                var subItem = properties[mapper].GetValue(item, null);
                mapper.OnTransientInitialized(mapContext, new[] { subItem });
            }
        }

        public void MapToOutput(MapContext<TDatabase> mapContext, object transient, object output)
        {
            var value = GetValue(transient);
            var mapper = this[value];
            var subItem = properties[mapper].GetValue(transient, null);
            mapper.MapToOutput(mapContext, subItem, output);
        }

        public void MapToInput(MapContext<TDatabase> mapContext, object output, object input)
        {
            var value = outputDiscriminatorSelector((TOutput)output);

            var item = typeMapping[value];
            var mapper = item.Item2;
            var existingEntity = item.Item3(mapContext, (TDatabaseEntity)input);
            var outputValue = output;

            var dbProperty = item.Item1.GetPropertyInfo();
            var entity = mapper
                .TransformToInput(mapContext, new[] { outputValue }, ids => existingEntity != null ? new[] { existingEntity } : new object[0])
                .SingleOrDefault();                    
            dbProperty.SetValue(input, entity, null);
        }

        public override string ToString()
        {
            return string.Format("DiscriminiatorMapping({0}, {1})", dbDiscriminatorSelector, outputDiscriminatorSelector);
        }
    }
}